| 1 | import { Stack, router, useLocalSearchParams } from "expo-router"; |
| 2 | import { GitPullRequest, ListTodo } from "lucide-react-native"; |
| 3 | import { useState } from "react"; |
| 4 | import { ActivityIndicator, FlatList, RefreshControl, View } from "react-native"; |
| 5 | |
| 6 | import { Checks, PullIcon } from "@/components/code"; |
| 7 | import { Segments } from "@/components/segments"; |
| 8 | import { Empty, ErrorView, Loading, Row, Screen, Text } from "@/components/ui"; |
| 9 | import { ItemRow } from "@/components/work"; |
| 10 | import { type ApiPull, useDescriptions, usePulls } from "@/lib/code"; |
| 11 | import { ago } from "@/lib/time"; |
| 12 | import { useWorkItems } from "@/lib/work"; |
| 13 | import { space, useColors } from "@/theme"; |
| 14 | |
| 15 | type Tab = "pulls" | "work"; |
| 16 | type State = "open" | "closed"; |
| 17 | |
| 18 | /** |
| 19 | * A repository: its pull requests, and its Work: the workspace's work |
| 20 | * items linked to it, in any role. |
| 21 | */ |
| 22 | export default function Repository() { |
| 23 | const { owner, repo } = useLocalSearchParams<{ owner: string; repo: string }>(); |
| 24 | const [tab, setTab] = useState<Tab>("pulls"); |
| 25 | const [state, setState] = useState<State>("open"); |
| 26 | const description = useDescriptions(owner).data?.[`${owner}/${repo}`.toLowerCase()]; |
| 27 | const pulls = usePulls(owner, repo, state); |
| 28 | const work = useWorkItems(owner, { state, repo }); |
| 29 | const list = tab === "pulls" ? pulls : work; |
| 30 | const colors = useColors(); |
| 31 | |
| 32 | return ( |
| 33 | <Screen> |
| 34 | <Stack.Screen options={{ title: repo }} /> |
| 35 | <View style={{ padding: space.lg, gap: space.md }}> |
| 36 | <Text size="small" tone="muted"> |
| 37 | {owner}/{repo} |
| 38 | {description ? ` · ${description}` : ""} |
| 39 | </Text> |
| 40 | <Segments |
| 41 | value={tab} |
| 42 | onChange={setTab} |
| 43 | options={[ |
| 44 | { value: "pulls", label: "Pull requests" }, |
| 45 | { value: "work", label: "Work" }, |
| 46 | ]} |
| 47 | /> |
| 48 | <Segments |
| 49 | value={state} |
| 50 | onChange={setState} |
| 51 | options={[ |
| 52 | { value: "open", label: "Open" }, |
| 53 | { value: "closed", label: "Closed" }, |
| 54 | ]} |
| 55 | /> |
| 56 | </View> |
| 57 | {list.isPending ? ( |
| 58 | <Loading /> |
| 59 | ) : list.error ? ( |
| 60 | <ErrorView error={list.error} retry={() => list.refetch()} /> |
| 61 | ) : tab === "pulls" ? ( |
| 62 | <FlatList |
| 63 | data={pulls.data} |
| 64 | keyExtractor={(pull) => pull.id} |
| 65 | refreshControl={<RefreshControl refreshing={pulls.isRefetching} onRefresh={() => pulls.refetch()} tintColor={colors.accent} />} |
| 66 | ListEmptyComponent={<Empty icon={GitPullRequest} title={`No ${state} pull requests`} />} |
| 67 | renderItem={({ item }) => <PullRow pull={item} owner={owner} repo={repo} />} |
| 68 | /> |
| 69 | ) : ( |
| 70 | <FlatList |
| 71 | data={work.items} |
| 72 | keyExtractor={(item) => item.id} |
| 73 | refreshControl={<RefreshControl refreshing={work.isRefetching && !work.isFetchingNextPage} onRefresh={() => work.refetch()} tintColor={colors.accent} />} |
| 74 | onEndReached={() => work.hasNextPage && !work.isFetchingNextPage && work.fetchNextPage()} |
| 75 | onEndReachedThreshold={0.5} |
| 76 | ListFooterComponent={work.isFetchingNextPage ? <ActivityIndicator color={colors.accent} style={{ padding: space.lg }} /> : null} |
| 77 | ListEmptyComponent={<Empty icon={ListTodo} title={`No ${state} work items`} body={`Work items linked to ${repo} show here.`} />} |
| 78 | renderItem={({ item }) => ( |
| 79 | <ItemRow item={item} onPress={() => router.push({ pathname: "/work/[number]", params: { number: String(item.number), workspace: item.workspace } })} /> |
| 80 | )} |
| 81 | /> |
| 82 | )} |
| 83 | </Screen> |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | function PullRow({ pull, owner, repo }: { pull: ApiPull; owner: string; repo: string }) { |
| 88 | const by = pull.author.display_username ?? pull.author.username; |
| 89 | return ( |
| 90 | <Row |
| 91 | leading={<PullIcon pull={pull} />} |
| 92 | title={pull.title} |
| 93 | subtitle={`#${pull.number} · ${by}${pull.agent ? ` with ${pull.agent}` : ""}${pull.item ? ` · for #${pull.item}` : ""} · ${ago(pull.updated_at)}`} |
| 94 | trailing={<Checks status={pull.check_status} />} |
| 95 | onPress={() => router.push({ pathname: "/code/[owner]/[repo]/pull/[number]", params: { owner, repo, number: String(pull.number) } })} |
| 96 | /> |
| 97 | ); |
| 98 | } |